前言

上一篇文章我们说到路由的正则编译,正则编译的目的就是和请求的 url 来匹配,只有匹配上的路由才是我们真正想要的,此外也会通过正则匹配来获取路由的参数。

路由的匹配

路由进行正则编译后,就要与请求 request 来进行正则匹配,并且进行一些验证,例如 UriValidatorMethodValidatorSchemeValidatorHostValidator

  1. class RouteCollection implements Countable, IteratorAggregate
  2. {
  3. public function match(Request $request)
  4. {
  5. $routes = $this->get($request->getMethod());
  6. $route = $this->matchAgainstRoutes($routes, $request);
  7. if (! is_null($route)) {
  8. return $route->bind($request);
  9. }
  10. $others = $this->checkForAlternateVerbs($request);
  11. if (count($others) > 0) {
  12. return $this->getRouteForMethods($request, $others);
  13. }
  14. throw new NotFoundHttpException;
  15. }
  16. protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
  17. {
  18. return Arr::first($routes, function ($value) use ($request, $includingMethod) {
  19. return $value->matches($request, $includingMethod);
  20. });
  21. }
  22. }
  23. class Route
  24. {
  25. public function matches(Request $request, $includingMethod = true)
  26. {
  27. $this->compileRoute();
  28. foreach ($this->getValidators() as $validator) {
  29. if (! $includingMethod && $validator instanceof MethodValidator) {
  30. continue;
  31. }
  32. if (! $validator->matches($this, $request)) {
  33. return false;
  34. }
  35. }
  36. return true;
  37. }
  38. public static function getValidators()
  39. {
  40. if (isset(static::$validators)) {
  41. return static::$validators;
  42. }
  43. return static::$validators = [
  44. new UriValidator, new MethodValidator,
  45. new SchemeValidator, new HostValidator,
  46. ];
  47. }
  48. }

UriValidator uri 验证

UriValidator 验证主要是目的是查看路由正则与请求是否匹配:

  1. class UriValidator implements ValidatorInterface
  2. {
  3. public function matches(Route $route, Request $request)
  4. {
  5. $path = $request->path() == '/' ? '/' : '/'.$request->path();
  6. return preg_match($route->getCompiled()->getRegex(), rawurldecode($path));
  7. }
  8. }

值得注意的是,在匹配路径之前,程序使用了 rawurldecode 来对请求进行解码。

MethodValidator 验证

请求方法验证:

  1. class MethodValidator implements ValidatorInterface
  2. {
  3. public function matches(Route $route, Request $request)
  4. {
  5. return in_array($request->getMethod(), $route->methods());
  6. }
  7. }

SchemeValidator 验证

路由 scheme 协议验证:

  1. class SchemeValidator implements ValidatorInterface
  2. {
  3. public function matches(Route $route, Request $request)
  4. {
  5. if ($route->httpOnly()) {
  6. return ! $request->secure();
  7. } elseif ($route->secure()) {
  8. return $request->secure();
  9. }
  10. return true;
  11. }
  12. }
  13. public function httpOnly()
  14. {
  15. return in_array('http', $this->action, true);
  16. }
  17. public function secure()
  18. {
  19. return in_array('https', $this->action, true);
  20. }

HostValidator 验证

主域验证:

  1. class HostValidator implements ValidatorInterface
  2. {
  3. public function matches(Route $route, Request $request)
  4. {
  5. if (is_null($route->getCompiled()->getHostRegex())) {
  6. return true;
  7. }
  8. return preg_match($route->getCompiled()->getHostRegex(), $request->getHost());
  9. }
  10. }

也就是说,如果路由中并不设置 host 属性,那么这个验证并不进行。

路由的参数绑定

一旦某个路由符合请求的 uri 四项认证,就将会被返回,接下来就要对路由的参数进行绑定与赋值:

  1. class RouteCollection implements Countable, IteratorAggregate
  2. {
  3. public function bind(Request $request)
  4. {
  5. $this->compileRoute();
  6. $this->parameters = (new RouteParameterBinder($this))
  7. ->parameters($request);
  8. return $this;
  9. }
  10. }

bind 函数负责路由参数与请求 url 的绑定工作:

  1. class RouteParameterBinder
  2. {
  3. public function parameters($request)
  4. {
  5. $parameters = $this->bindPathParameters($request);
  6. if (! is_null($this->route->compiled->getHostRegex())) {
  7. $parameters = $this->bindHostParameters(
  8. $request, $parameters
  9. );
  10. }
  11. return $this->replaceDefaults($parameters);
  12. }
  13. }

可以看出,路由参数绑定分为主域参数绑定与路径参数绑定,我们先看路径参数绑定:

路径参数绑定

  1. class RouteParameterBinder
  2. {
  3. protected function bindPathParameters($request)
  4. {
  5. preg_match($this->route->compiled->getRegex(), '/'.$request->decodedPath(), $matches);
  6. return $this->matchToKeys(array_slice($matches, 1));
  7. }
  8. }

例如,{foo}/{baz?}.{ext?} 进行正则编译后结果:

  1. #^/(?P<foo>[^/]++)(?:/(?P<baz>[^/\.]++)(?:\.(?P<ext>[^/]++))?)?$#s

其与 request 匹配后的结果为:

  1. $matches = array (
  2. 0 = "/foo/baz.ext",
  3. 1 = "foo",
  4. foo = "foo",
  5. 2 = "baz",
  6. baz = "baz",
  7. 3 = "ext",
  8. ext = "ext",
  9. )

array_slice($matches, 1) 取出了 $matches 数组 1 之后的结果,然后调用了 matchToKeys 函数,

  1. protected function matchToKeys(array $matches)
  2. {
  3. if (empty($parameterNames = $this->route->parameterNames())) {
  4. return [];
  5. }
  6. $parameters = array_intersect_key($matches, array_flip($parameterNames));
  7. return array_filter($parameters, function ($value) {
  8. return is_string($value) && strlen($value) > 0;
  9. });
  10. }

该函数中利用正则获取了路由的所有参数:

  1. class Route
  2. {
  3. public function parameterNames()
  4. {
  5. if (isset($this->parameterNames)) {
  6. return $this->parameterNames;
  7. }
  8. return $this->parameterNames = $this->compileParameterNames();
  9. }
  10. protected function compileParameterNames()
  11. {
  12. preg_match_all('/\{(.*?)\}/', $this->domain().$this->uri, $matches);
  13. return array_map(function ($m) {
  14. return trim($m, '?');
  15. }, $matches[1]);
  16. }
  17. }

可以看出,获取路由参数的正则表达式采用了勉强模式,意图提取出所有的路由参数。否则,对于路由 {foo}/{baz?}.{ext?},贪婪型正则表达式 /\{(.*)\}/ 将会匹配整个字符串,而不是各个参数分组。

提取出的参数结果为:

  1. $matches = array (
  2. 0 = array (
  3. 0 = "{foo}".
  4. 1 = "{baz?}",
  5. 2 = "{ext?}",
  6. )
  7. 1 = array (
  8. 0 = "foo".
  9. 1 = "baz?",
  10. 2 = "ext?",
  11. )
  12. )

得出的结果将会去除 $matches[1],并且将会删除结果中最后的 ?

之后,在 matchToKeys 函数中,

  1. $parameters = array_intersect_key($matches, array_flip($parameterNames));

获取了匹配结果与路由所有参数的交集:

  1. $parameters = array (
  2. foo = "foo",
  3. baz = "baz",
  4. ext = "ext",
  5. )

主域参数绑定

  1. protected function bindHostParameters($request, $parameters)
  2. {
  3. preg_match($this->route->compiled->getHostRegex(), $request->getHost(), $matches);
  4. return array_merge($this->matchToKeys(array_slice($matches, 1)), $parameters);
  5. }

步骤与路由参数绑定一致。

替换默认值

进行参数绑定后,有一些可选参数并没有在 request 中匹配到,这时候就要用可选参数的默认值添加到变量 parameters 中:

  1. protected function replaceDefaults(array $parameters)
  2. {
  3. foreach ($parameters as $key => $value) {
  4. $parameters[$key] = isset($value) ? $value : Arr::get($this->route->defaults, $key);
  5. }
  6. foreach ($this->route->defaults as $key => $value) {
  7. if (! isset($parameters[$key])) {
  8. $parameters[$key] = $value;
  9. }
  10. }
  11. return $parameters;
  12. }

匹配异常处理

如果 url 匹配失败,没有找到任何路由与请求相互匹配,就会切换 method 方法,以求任意路由来匹配:

  1. protected function checkForAlternateVerbs($request)
  2. {
  3. $methods = array_diff(Router::$verbs, [$request->getMethod()]);
  4. $others = [];
  5. foreach ($methods as $method) {
  6. if (! is_null($this->matchAgainstRoutes($this->get($method), $request, false))) {
  7. $others[] = $method;
  8. }
  9. }
  10. return $others;
  11. }

如果使用其他方法匹配成功,就要判断当前方法是否是 options,如果是则直接返回,否则报出异常:

  1. protected function getRouteForMethods($request, array $methods)
  2. {
  3. if ($request->method() == 'OPTIONS') {
  4. return (new Route('OPTIONS', $request->path(), function () use ($methods) {
  5. return new Response('', 200, ['Allow' => implode(',', $methods)]);
  6. }))->bind($request);
  7. }
  8. $this->methodNotAllowed($methods);
  9. }
  10. protected function methodNotAllowed(array $others)
  11. {
  12. throw new MethodNotAllowedHttpException($others);
  13. }